package com.cordys.opensource.crom.sql;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.cordys.cpc.bsf.busobject.BusObject;
import com.cordys.cpc.bsf.busobject.BusObjectIterator;
import com.cordys.cpc.bsf.busobject.QueryObject;
import com.cordys.opensource.crom.sql.Where.Operator;

public class Query {
    private int _nrRows = 0;

	private final List<Where> whereParts = new ArrayList<Where>();
	private final List<Column> orderParts = new ArrayList<Column>();
	private final List<Query> unionParts = new ArrayList<Query>();
	private final Map<String, Table> tables = new LinkedHashMap<String, Table>();
	private final List<Query> nestedQueries = new ArrayList<Query>();
	
	/**
	 * Contains a list of all XQY parameters in this query.
	 */
	private final Map<String, XQYParameter> parameters = new HashMap<String, XQYParameter>();
	// The prefix for the XQY parameters. This is required in case of nested queries to be 
	//  able to make all parameters unique on the single bsf query object
	private String xqyParameterNamePrefix = "";
	
    
    public Query()
    {
    }
    
    public Query(Class<? extends BusObject> persistenceClass)
    {
    	this.setResultClass(persistenceClass);
    	this.getTable(persistenceClass).selectAllColumns();
    }

    /**
     * Returns a table for the specified WS-AppServer class
     * @param persistenceClass
     * @return
     */
	public Table getTable(Class<? extends BusObject> persistenceClass)
	{
		String tableName = BusObject._getClassInfo(persistenceClass).getTableName();
		return getTable(persistenceClass, tableName);
	}
	
	public Table getTable(Class<? extends BusObject> persistenceClass, String aliasName)
	{
		Table table = tables.get(aliasName);
		if (table==null)
		{
			table = new Table(this, persistenceClass, aliasName);
			tables.put(aliasName, table);
		}
		return table;
	}
	
	public Collection<Table> getTables()
	{
		return tables.values();
	}
	
	/**
	 * Removes a table along with all it's related joins from the query
	 * @param table
	 */
	public void removeTable(Table table)
	{
		tables.remove(table.getAliasName());
	}
	
	void addXQYParameter(XQYParameter parameter)
	{
		String columnName = parameter.getColumn().getColumnName();
		String parameterName = columnName;
		int i = 0;
		while (parameters.containsKey(parameterName))
		{
			parameterName = columnName + (i++);
		}
		parameter.setParameterName(parameterName);
		this.parameters.put(parameterName, parameter);
	}
	
	/**
	 * Internal method to be used by the Column class only.
	 * It is used to put the columns into the OrderBy clause in the proper insertion order
	 * @param column
	 */
	void addOrder(Column column)
	{
		orderParts.add(column);
	}
	
	public Where where(String expression)
	{
		Where where = new Where(this, expression);
		whereParts.add(where);
		return where;
	}
	
	public Where where(Column column, Operator operator, Object operand)
	{
		Where where = new Where(this, "", column, "", operator, "", operand, "");
		whereParts.add(where);
		return where;
	}

	public Where where( String columnPrefix
			          , Column column
			          , String columnPostfix
			          , Operator operator
			          , String operandPrefix
			          , Object operand
			          , String operandPostfix )
	{
		Where where = new Where(this, columnPrefix, column, columnPostfix, operator, operandPrefix, operand, operandPostfix);
		whereParts.add(where);
		return where;
	}
	
	public Where whereExists(Query nestedQuery)
	{
		Where where = new Where(this, nestedQuery);
		whereParts.add(where);
		return where;
	}
	
	public Query union(Query q)
	{
		unionParts.add(q);
		return q;
	}
	
	public Query[] getUnion()
	{
		return unionParts.toArray(new Query[unionParts.size()]);
	}
	
	public void numberOfRows(int nrRows)
	{
		_nrRows = nrRows;
	}
	
	private Class<? extends BusObject> resultClass;
	private boolean transientResult = true;
	private int resultMapping = QueryObject.MAP_FLATTEN;
	
	/**
	 * Sets the result class of the query. By default this class is
	 * returned as transient, unless specified otherwise.
	 * @param resultClass
	 * @param makeTransient
	 */
	public void setResultClass(Class<? extends BusObject> resultClass, boolean makeTransient)
	{
		this.resultClass = resultClass;
		this.transientResult = makeTransient;
	}
	
	public void setResultClass(Class<? extends BusObject> resultClass)
	{
		setResultClass(resultClass, this.transientResult);
	}
	
	/**
	 * Sets whether the result class
	 * @param transientResult
	 */
	public void makeResultTransient(boolean transientResult)
	{
		this.transientResult = transientResult;
	}
	
	/**
	 * Sets the result mapping of the query. By default this is QueryObject.MAP_FLATTEN
	 * @param resultMapping
	 */
	public void setResultMapping(int resultMapping)
	{
		this.resultMapping = resultMapping;
	}
	
	/**
	 * Adds the XQY parameters to the bsf query object.
	 * Also for the nested queries and their nested queries, etc.
	 * @param bsfQueryObject
	 */
	private void addXQYParameters(QueryObject bsfQueryObject)
	{
		for (XQYParameter parameter : parameters.values())
		{
			parameter.addToQueryObject(bsfQueryObject);
		}
		for (Query nestedQuery : nestedQueries)
		{
			nestedQuery.addXQYParameters(bsfQueryObject);
		}
	}
	
	public <T extends BusObject> BusObjectIterator<T> getObjects()
	{
		String sql = this.toString();
  		QueryObject bsfQueryObject = new QueryObject(sql);
  		addXQYParameters(bsfQueryObject);
		bsfQueryObject.setResultClass(resultClass);
		bsfQueryObject.setTransientResult(transientResult);
		bsfQueryObject.setResultMapping(this.resultMapping, null);
		return bsfQueryObject.getObjects();
	}

	@SuppressWarnings("unchecked")
	public <T extends BusObject> T getObject()
	{
		String sql = this.toString();
  		QueryObject bsfQueryObject = new QueryObject(sql);
  		addXQYParameters(bsfQueryObject);
		bsfQueryObject.setResultClass(resultClass);
		bsfQueryObject.setTransientResult(transientResult);
		bsfQueryObject.setResultMapping(this.resultMapping, null);
		return (T) bsfQueryObject.getObject();
	}

	public String toString()
	{
		final StringBuilder sb = new StringBuilder();

		
		final StringBuilder selectString = new StringBuilder("select");
		
		if (_nrRows > 0) selectString.append(" TOP(" + _nrRows + ")");
		
		if (tables.size() > 0)
		{
			boolean isFirstSelect = true;
			boolean isFirstFrom = true;
			for (Table table : tables.values())
			{
				String selectedColumns = table.getSelectString();
				if (selectedColumns.length()>0)
				{
					if (!isFirstSelect)
					{
						selectString.append(",\n\n");
					}
					selectString.append(table.getSelectString());
					isFirstSelect = false;
				}
				
				if (table.hasBeenJoined())
				{
					// The table with which this table is joined will take care
					//  of printing the table name (in the proper order).
					continue;
				}
				
				if (isFirstFrom)
				{
					sb.append("\n from "); // also add the FROM statement.
				}
				else
				{
					sb.append(", ");
				}
				sb.append(table.toString());
				isFirstFrom = false;
			}
		}
		if (whereParts.size() > 0)
		{
			sb.append("\n where");
			for (Where s : whereParts)
			{
				sb.append(" " + s.toString() + " and");
			}
			sb.delete(sb.length()-4, sb.length());
		}
		if (orderParts.size() > 0)
		{
			sb.append("\n order by");
			for (Column column : orderParts)
			{
				Order ordering = column.getOrdering();
				if (ordering!=Order.NONE)
				{
					sb.append(" " + column.toString() + column.getOrdering() + ",");
				}
			}
			sb.delete(sb.length()-1, sb.length());
		}
		if (unionParts.size() > 0)
		{
			sb.append(" union");
			for (Query q : unionParts)
			{
				sb.append(" " + q.toString());
			}
		}
		
		return selectString.toString() + sb.toString();
	}
	
	/**
	 * Returns the parameter prefix for this query that can be used
	 * to create unique parameter names
	 * @return
	 */
	String getXQYParameterNamePrefix()
	{
		return this.xqyParameterNamePrefix;
	}
	
	/**
	 * Renames the xqy parameters if this query is
	 * added as a nested query to another query object.
	 * Also renames all the inner nested queries
	 * @param queryPrefix
	 */
	private void renameParameterPrefix(String queryPrefix)
	{
		this.xqyParameterNamePrefix = queryPrefix;
		for (Query query : this.nestedQueries)
		{
			query.renameParameterPrefix(queryPrefix+this.nestedQueries.indexOf(query)+"_");
		}
	}

	/**
	 * Adds a nested query. Typically when the where clause contains an IN or an EXISTS statement
	 * This methods makes sure inner parameters are also added and made unique
	 * @param nestedQuery
	 */
	void addNestedQuery(Query nestedQuery)
	{
		this.nestedQueries.add(nestedQuery);
		// Now give the nested query a unique parameter prefix
		nestedQuery.renameParameterPrefix("q"+nestedQueries.size()+"_");
	}
}
